﻿using Generator;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;

namespace ALDIOrganizer
{    

    /// <summary>
    /// Contains the actual Rota sheet (Shifts and contract_hours), serves as a "sheet" for the RotaPackaging class
    /// </summary>
    /// <remarks>
    /// Sheet doesn't store the information about _staff - for example. It is able to "find out"
    /// who works On
    /// Sheet state is used as a snapshot for Undo/Redo functionality.
    /// </remarks>
    [Serializable]
    [DebuggerDisplay("{bndDays} - events {(OnDataChanged!=null)?\"attached\":\"not attached\"}")]
    public partial class Rota : IDeepCopyable<Rota>
    {        
        bool _empty;
        bool Empty
        {
            get
            {
                return _empty;
            }
            set
            {
                _empty = value;
                TriggerPropertyChanged("bndAction");
            }
        }
        public bool IsEmpty // read-only
        {
            get { return Empty; }
        }

        internal RotaAction ProperAction
        {
            get
            {
                if (Program.Logged.CanEditRota(Monday))
                    return IsEmpty ?
                        RotaAction.Create :
                        RotaAction.Edit;

                return IsEmpty ? // if cannot edit this one
                    RotaAction.None :
                    RotaAction.View;                
            }
        }
                
        #region Fields associated with its date
        /* When does that particular GetParent Start?
         * The id1 Day of the week is always date, hence such Name. */
        public readonly DateTime Monday;
        // Returns the last Day of the Shift. Read-only
        public DateTime Sunday
        {
            get { return this.Monday.AddDays(6); }
        }
        // Read-only. Which week of the year does this GetParent represent?
        public int WhichWeek
        {
            get { return Monday.WeekOfYear(); } 
        }

        // Read-only. Is the GetParent ALL in the past? False as long as it'pShift still Sunday.
        public bool HasPassed
        {
            get
            {
                return (Sunday.Date < DateTime.Now.Date);
            }
        }
        
        #endregion

        [NonSerialized]
        public Staff Employees;

        public Rota(DateTime pMonday, Template pTemplate)
        {
            Monday = pMonday;
            InitializeHolidaysLists();
            ActivateEmpty(pTemplate);
        }

        /// <summary>
        /// Empty
        /// </summary>
        /// <param Name="pMonday"></param>
        Rota(DateTime pMonday)
        {
            Monday = pMonday;
            InitializeHolidaysLists(); // they will be used even for an empty one            
            Empty = true;
        }

        public static Rota GetEmpty(DateTime pMonday)
        {
            return new Rota(pMonday);
        }

        public delegate void OnDayStatusesUpdatedEventHandler(Weekday[] pDays);
        public event OnDayStatusesUpdatedEventHandler OnDayStatusesUpdated;
        public delegate bool delOnShuttingDayWithShifts(Weekday[] days);
        public event delOnShuttingDayWithShifts OnShuttingDayWithShifts;
        public delegate void OnViolatingRequestEventHandler(Int64 pEmployeeID, Request[] pRequests);
        public event OnViolatingRequestEventHandler OnViolatingRequest;

        public override event OnDataChangedEventHandler OnDataChanged;

        public void AllRefresh()
        {
            if (OnDataChanged != null)
            {
                OnDataChanged(GetAllTags, Time.AllWeekdays);
            }
        }


        public SpecialDayStatus[] DayStatuses;
        public bool[] StatusesAsBoolArray(SpecialDayStatus pWhich)
        {
            return (from _status in DayStatuses
                    select _status == pWhich).ToArray();
        }
        
        [NonSerialized]
        private WorkingHoursSpan[] _WorkingHours;

        public WorkingHoursSpan[] GetWorkingHours
        {
            get { return _WorkingHours; }
        }

        public void SetWorkingHours(WorkingHoursSpan pHours, Weekday pDay)
        {
            _WorkingHours[pDay.ToInt0()] = pHours;
        }

        private string _TemplateName;
        public string TemplateName
        {
            get { return _TemplateName; }
        }

        public void SetTemplate(string pTemplateName, WorkingHoursSpan[] pHours)
        {
            var _old_name = _TemplateName;
            _TemplateName = pTemplateName;            
            _WorkingHours = pHours;
            if (_old_name == _TemplateName)
            {
                return;
            }
            Modified = true;            
            if (OnDataChanged != null)
            {
                AllRefresh();
            }
        }
        
        /// <summary>
        /// Reloads employees (based on Contracts datatables) and forces the Sheet to adjust its rows accordingly
        /// </summary>
        /// <remarks>
        /// This should be called whenever the Contracts datatable is modified
        /// (eg. somebody was Hired, or fired, or just had some details of their contracts changed).
        /// 
        /// We call Sheet.PrepareRows, forcing the Sheet to either AddAnotherRowString or remove rows, depending On whether there'pShift
        /// more people coming or some leaving.
        /// 
        /// If the Sheet is Bound to a SheetView (which will happen if it'pShift currently displayed On the form -
        /// - then its PrepareRows method will let the DataGridView know what happened (by triggering the event) and so the 
        /// DataGridView is also going to react, by redrawing itself.
        /// </remarks>
        public void Refresh()
        {            
            PrepareRows(Employees.Refresh());
        }

        public void RefreshEmployeeRow(Int64 pEmpID)
        {
            var employee = Employees.GetEmployee(pEmpID);
            if (employee == null)
            {
                return;
            }
            employee.RecalculateContractsHolidaysAndPosition(
                true,
                true,
                true,
                Monday);            
            foreach (var cell in this[pEmpID])
            {
                cell.Update();
            }                        
        }
        public List<Shift> GetShiftsForEmployee(Int64 empID)
        {
            var row_in_question = this[empID];
            return GetAllShiftsForRow(row_in_question);
        }

        public ShiftTandem GetShiftsForEmployeeOn(Int64 empID, Weekday day)
        {
            var row_in_question = this[empID];
            return row_in_question[day.ToInt1()].Shifts;
        }
        
        public void AddRow(Int64 empID)
        {
            if (Mode != SheetType.Rota)
            {
                return;
            }
            var newRow = SpawnNewRow(Mode, this, new RowTag(empID));
            var index = 0;
            var insertedEmployee = newRow[0].GetMyEmployee;                
            while(index < Cells.Count)
            {
                var otherGuy = Cells[index][0].GetMyEmployee;                    
                var here = (otherGuy.CompareTo(insertedEmployee)) >= 0;                                                                                
                if (here)
                {                        
                    break;
                }
                index++;
            }
            Cells.Insert(
                index,
                newRow);                            
        }

        public void RemoveRow(Int64 empID)
        {
            if (Mode != SheetType.Rota)
            {
                return;
            }
            var rowInQuestion = this[empID];
            Cells.Remove(rowInQuestion);
        }       
        

        /// <summary>
        /// Causes Sheet to update its rows based On an up-to-date list of _employee GetEmployeeIDs
        /// </summary>
        /// <param Name="freshList">
        /// List of employees'pShift GetEmployeeIDs - those who are employed in that particular week
        /// </param>
        public void PrepareRows(IEnumerable<Int64> freshList)
        {
            /* We are answering two questions: what rows are missing (some GetEmployeeIDs are On the freshList,
             * but we don't have rows for them - eg. somebody was Hired, and we need to make space)
             * and what rows are to be removed (some GetEmployeeIDs that we locally have are not present On the 
             * freshList - we stored some Shifts for a guy whose employment could have been terminated). */

            // GetEmployeeIDs of the employees ones which are in the sheet RIGHT NOW
            var who_is_now_QUERY = from row in GetAllRows
                                   select row[0].EmployeeID;

            var who_is_now = who_is_now_QUERY.ToArray();    // executes the query
            var who_should_be = freshList.ToArray();        // executes the query

            /* Now we retrieve differences between two arrays.
             * Descriptive names of those queries are (hopefully) self-explanatory. */
            var who_is_missing = (from who in who_should_be
                                  where !who_is_now.Contains(who)
                                  select who).ToArray();
            var whom_to_remove = (from who in who_is_now
                                  where !who_should_be.Contains(who)
                                  select who).ToArray();

            // There we go!
            foreach (var id in whom_to_remove)
                RemoveRow(id);
            foreach (var id in who_is_missing)
                AddRow(id);

            // trigger an event here
        }

        

        /// <summary>
        /// Returns all the Shifts from the Sheet which collide with latest modifications of Holidays or Requests datatable.
        /// </summary>
        /// <param Name="whose">
        /// id of the _employee whose data was modified. Pass -1 if you want to check everybody'pShift data.
        /// </param>
        /// <param Name="tables_to_be_checked_against">
        /// Which tables to check against.
        /// </param>
        /// <returns>
        /// All the Shifts On the Sheet colliding with latest modifications of given datatables for the given _employee.
        /// </returns>
        public IEnumerable<Shift> FindIncompatibleShifts(Int64 whose, int[] tables_to_be_checked_against)
        {
            var ids = SQLiteAccess.GetEmployeeIDs(whose);

            foreach (var id in ids)
            {
                for (var day = 0; day < 7; day++)
                {
                    var guy = Employees.GetEmployee(id);
                    var his_shifts = GetShiftsForEmployee(id);
                    if ((tables_to_be_checked_against.Contains(1) && !guy.HolidayStatuses.Key[day])
                        | (tables_to_be_checked_against.Contains(2))) // TU!
                        foreach (var shift in his_shifts)
                            yield return shift;
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param Name="status"></param>
        /// <param Name="pDays"></param>
        /// <remarks>
        /// Attention: it "unsets" this status for all pDays other than pDays!
        /// </remarks>
        internal void SetSpecialStatus(SpecialDayStatus status, Weekday[] pDays)
        {
            #region take care of mismatches (only if you're shutting the store)
            if (status.IsShut())
            {
                var total = TotalHours();
                var anythingToDelete = pDays.Any(day => total[day.ToInt0()].GetTotal > 0);
                if (anythingToDelete)
                {
                    /* call event!
                     * this event handler informs us whether we should delete the Shifts (for the Day the store is supposed to be Shut)
                     * or not */
                    var delete = OnShuttingDayWithShifts(pDays);
                    if (delete)
                    {
                        foreach (var day in pDays)
                        {
                            ClearColumn(day);
                        }
                    }
                    else
                    {
                        return;
                    }
                }
            }
            #endregion

            #region actually updating

            var updated = new List<Weekday>();

            foreach (var _day in pDays)
            {
                DayStatuses[_day.ToInt0()] = status;
            }

            updated.AddRange(pDays);
            
            foreach (var day in pDays.Remaining().Where(day => DayStatuses[day.ToInt0()] == status))
            {
                DayStatuses[day.ToInt0()] = SpecialDayStatus.None;
                updated.Add(day);
            }

            #endregion

            OnDayStatusesUpdated(updated.ToArray());
            Sounds.Play(Sounds.Validated);
            Modified = true;
        }

        public SumOfHours GetSumOfContractHours
        {
            get
            {
                SumOfHours result;
                result.OnProductivity = (from _employee in Employees.getWorkers
                                where !_employee.IsOffProductivity
                                select _employee.ContractHours).Sum();
                result.OffProductivity = (from _employee in Employees.getWorkers
                                   where _employee.IsOffProductivity
                                   select _employee.ContractHours).Sum();
                return result;
            }
        }

        public IEnumerable<Int64> MondayOpeners
        {
            get
            {
                return from _shift in GetAllShiftsForColumn(1)
                       where (_shift.opening)
                       | ((_shift.Start < GetWorkingHours[0].Open)
                       & (!(_shift.Finish < GetWorkingHours[0].Open)))
                       select _shift.EmployeeID;
            }
        }

        public IEnumerable<Int64> SundayClosers
        {
            get
            {
                return from _shift in GetAllShiftsForColumn(7)
                       where (_shift.closing)
                       | ((_shift.Finish > GetWorkingHours[6].Close)
                       & (!(_shift.Start > GetWorkingHours[6].Close)))
                       select _shift.EmployeeID;
            }
        }


        internal Cell[] BottomBar { get; private set; }

        internal void UpdateBottomBar(Weekday[] days)
        {
            BottomBar[0].Update(true, false);
            BottomBar[0].Value = GetSumOfContractHours.GetTotal.ToString("N");

            var total = TotalHours();
            foreach (var day in days)
            {
                BottomBar[day.ToInt1()].Update(true, false);
                BottomBar[day.ToInt1()].Value = total[day.ToInt0()].GetTotal.ToString("N");
            }            
        }

        internal virtual void AddShifts(ShiftTandem shifts, Weekday day, RowTag rowtag, bool refreshAndClick)
        {
            foreach (Shift s in shifts)
            {
                s.Parent = this;
                s.Day = day;
                s.EmployeeID = rowtag.Int64Value;                    
                addShift(day, rowtag, s);
            }
            var cell = this[rowtag][day.ToInt1()];

            if (!refreshAndClick)
            {
                return;
            }
            
            OnDataChanged(
                new[] { rowtag },
                new[] { day });
                
            Sounds.Play(Sounds.Validated);
            if (!cell.HasAnyRequests)
            {
                return;
            }
            var _requests = cell.GetMyRequests;
            var _colliding_requests = _requests.WhichCollideWith(shifts.ToArray(),
                                                                 true);
                    
            if ((_colliding_requests != null) &&
                (_colliding_requests.Length > 0) &&
                (OnViolatingRequest != null))
            {
                OnViolatingRequest(
                    rowtag.Int64Value,
                    _colliding_requests);
            }
        }

        internal virtual void RemoveShifts(Weekday day, RowTag rowtag, bool refreshAndClick)
        {
            var cell = this[rowtag][day.ToInt1()];
            removeShifts(cell);
            if (refreshAndClick)
            {
                // OnMustRecalculateDiscrepancies(new RowTag[] { rowtag }, new Weekday[] { Day });
                OnDataChanged(new[] { rowtag }, new[] { day });
                Sounds.Play(Sounds.Validated);
            }
        }

        internal protected void ColumnDataChanged(Weekday pDay)
        {
            if (OnDataChanged == null)
            {
                return;
            }
            OnDataChanged(
                GetAllTags,
                new[] { pDay });
        }

        internal virtual void RemoveShiftsEntireRow(RowTag pRowTag, bool pRefreshAndClick)
        {
            for (var _day = 1; _day < 8; _day++)
            {
                var _cell = this[pRowTag][_day];
                removeShifts(_cell);
            }
            if (!pRefreshAndClick)
            {
                return;
            }            
            OnDataChanged(
                new[] { pRowTag },
                Time.AllWeekdays);
            Sounds.Play(Sounds.Validated);
        }

        internal void AssignTemplate(Template pTemplate)
        {
            _WorkingHours = pTemplate.WorkingHours;
            _TemplateName = pTemplate.Name;
        }       

        internal void ActivateEmpty(Template pTemplate)
        {
            Activate();
            Empty = false;
                        
            DayStatuses = new SpecialDayStatus[7]; // only for empty
            AssignTemplate(pTemplate);
            BottomBar = new Cell[cols];
            for (int i = 0; i < cols; i++)
            {
                BottomBar[i] = new Cell(this, i, new RowTag(-1))
                {
                    MyCellType = CellType.TotalHours
                };
            }
            UpdateBottomBar(Time.AllWeekdays);
            Refresh();
        }

        /// <summary>
        /// Initializes non-serialized fields
        /// </summary>
        internal void Activate()
        {
            Employees = new Staff(this);
        }

        #region IDeepCopyable members
        public Rota DeepCopy()
        {
            #region ---=== NOTE TO SELF ===---
            /* ---=== NOTE TO SELF ===---
             * 
             * Why wouldn't I use serialization for the entire class, rather than deep-copying
             * field by field?
             * 
             * The reason is that there are some problems with cloning by serialization caused by events.
             * 
             * .NET serialization tries to serialize the LISTENERS of our events, too.
             * 
             * It means that if - for example - the OnDataChanged event of this class 
             * is Bound to a SheetView control, then cloning by serialization would
             * Finish up with an attempt to serialize/deserialize the entire control.
             * 
             * There is no simple solution to this problem - events cannot be marked as [NonSerialized].
             * I found an article On DevX.com which suggests three solutions:
             * 
             * QUOTE:
             * 1. "You could clone the object before serialization [...] This is very convenient
             * [...] if the cloning process does not use serialization!"
             * 
             * - but ours does
             * 
             * 2. "You could implement ISerializable and a personal serialization, serializing all
             * fields but not the EventNameEvent ones"
             * 
             * - bit of an overkill? I don't know
             * 
             * 3. "You could detach events from the object before serialization and then reattach
             * them to the object."
             * 
             * - this makes the code harder to understand since "the outer world" (the rest of the application,
             * outside of the scope of this class) has to take note When this instance will need to be rebound
             * to listeners (have all events reattached to it) and I think this is ugly and bug-prone,
             * When you modify the application at some point you will forget about this and get erratic
             * behaviour and it won't be very obvious why.
             * 
             * maybe I'm wrong */
            #endregion

            // brand new Copy
            var _copy = new Rota(
                new DateTime(Monday.Ticks),
                Program.CurrentFile.GetTemplateCopy(TemplateName));

            if (BottomBar != null)
            {
                _copy.BottomBar = Cloner.Copy(BottomBar);
            }
            if (Cells != null)
            {
                _copy.Cells = Cloner.Copy(Cells);
            }
            if (DayStatuses != null)
            {
                _copy.DayStatuses = Cloner.Copy(DayStatuses);
            }

            _copy.InitializeHolidaysLists();            
            _copy.OnHolidaysALLWEEK.AddRange(OnHolidaysALLWEEK.ToArray());            
            _copy.OnHolidaysLEAVING.AddRange(OnHolidaysLEAVING.ToArray());            
            _copy.OnHolidaysPARTLY.AddRange(OnHolidaysPARTLY.ToArray());            
            _copy.OnHolidaysRETURNING.AddRange(OnHolidaysRETURNING.ToArray());

            _copy.SetParent(_copy);

            _copy.Employees = new Staff(_copy);
            _copy.ActivateCellStyles();
            
            _copy.ResetTracker();

            
            return _copy;
        }
        #endregion

        internal void SetParent(Rota pParent)
        {
            foreach (var _shift in GetAllShifts)
                _shift.SetParent(pParent);            

            foreach (var _cell in GetAllCells)
                _cell.SetParent(pParent);
        }

        internal IEnumerable<Cell> GetAllCells
        {
            get
            {
                var allRows = GetAllRows.ToArray().Concat(new[] { BottomBar }).ToArray();
                foreach (var _row in allRows)
                {
                    if (_row == null)
                    {
                        continue;
                    }
                    foreach (var _cell in _row)
                    {
                        yield return _cell;
                    }
                }
            }
        }

        
        [NonSerialized]
        internal Report FinalReport;

        
    }
}
